Going further with CDI 1.2

Antoine Sabot-Durand

  • Senior Software Engineer
  • CDI co-spec lead
  • Red Hat, Inc.
  • @antoine_sd
  • www.next-presso.com
  • github.com/antoinesd

Antonin Stefanutti

  • Software Engineer
  • Murex, SAS
  • @astefanut
  • github.com/astefanutti

Agenda

  • CDI Extensions
  • Legacy Code
  • Camel CDI
  • Metrics CDI

CDI Extensions

Portable Extensions

One of the most powerful feature of the CDI specification

Not really popularized in partly due to the high level of abstraction

powerful

Container Metadata

Observer pattern to listen for container initialization lifecycle events

Comprehensive access to and modification of the container metadata model

rubik

Plugin Architecture

Service provider of the service javax.enterprise.inject.spi.Extension declared in META-INF/services
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.Extension;

class CdiExtension implements Extension {

    void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd) {
    }
    ...

    void afterDeploymentValidation(@Observes AfterDeploymentValidation adv) {
    }
}

Lifecycle Events

lifecycle

Legacy code

Injection points, parameterized types, programmatic bean

A (not so fictional) Legacy Component

The CDI Way

A functional interface:
public interface Transformer<I, O> {
    O transform(I input);
}
And a CDI qualifier with transformation metadata:
import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;

@Qualifier
public @interface Transformation {
    @Nonbinding
    String value() default ""; // The transformation name
}

Configuration at Injection Points

@Inject
@Transformation("transformation")
Transformer<InputType, OutputType> transformer;

InputType input;
OutputType result = transformer.transform(input);
Distribute configuration closest to the code:
@Inject
@Transformation("transformationi")
Transformer<InputTypei, OutputTypei> transformeri;
...
@Inject
@Transformation("transformationj")
Transformer<InputTypej, OutputTypej> transformerj;

The ProcessInjectionPoint Event

Collect all the @Transformation metadata required to instantiate the legacy component
public interface ProcessInjectionPoint<T, X> {
    public InjectionPoint getInjectionPoint();
    public void setInjectionPoint(InjectionPoint injectionPoint);
    public void addDefinitionError(Throwable t);
}

Collect Injection Points Metadata

class TransformationExtension implements Extension {

  Set<String> transformations = new HashSet<>();

  void collectConfiguration(@Observes ProcessInjectionPoint<?, Transformer> pit) { (1)
    Annotated annotated = pit.getInjectionPoint().getAnnotated();
    if (annotated.isAnnotationPresent(Transformation.class)) {
      transformations.add(annotated.getAnnotation(Transformation.class).value()); (2)
    }
  }
}
1Observe every Transformer injection point on any declaring bean
2Collect the injection point @Transformation metadata

The Bean Interface

Integrate the legacy component as a CDI Bean
public interface Bean<T> extends Contextual<T>, BeanAttributes<T> {
    public Class<?> getBeanClass();
    public Set<InjectionPoint> getInjectionPoints();
    // Contextual<T>
    public T create(CreationalContext<T> creationalContext);
    public void destroy(T instance, CreationalContext<T> creationalContext);
    // BeanAttributes<T>
    public Set<Type> getTypes();
    public Set<Annotation> getQualifiers();
    public Class<? extends Annotation> getScope();
    public String getName();
    public Set<Class<? extends Annotation>> getStereotypes();
    public boolean isAlternative();
}

Define Beans Programmatically

class LegacyTransformerBean implements Bean<LegacyTransformer> {
    Set<String> transformations = new HashSet<>();
    LegacyTransformerBean(Set<String> transformations) {
        this.transformations = transformations;
    }
    public LegacyTransformer create(CreationalContext<LegacyTransformer> context) {
        LegacyTransformer transformer = new LegacyTransformer();
        tranformer.load(transformations);
        return LegacyTransformer;
    }
    public Set<Annotation> getQualifiers() {
        return Collections.unmodifiableSet(new HashSet<>(
            Arrays.asList(DefaultLiteral.INSTANCE, AnyLiteral.INSTANCE)));
    }
    public Class<? extends Annotation> getScope() {
        return ApplicationScoped.class;
    }
}

The AfterBeanDiscovery Event

Add the legacy component bean after bean discovery
public interface AfterBeanDiscovery {
    public void addDefinitionError(Throwable t);
    public void addBean(Bean<?> bean);
    public void addObserverMethod(ObserverMethod<?> observerMethod);
    public void addContext(Context context);
    public <T> AnnotatedType<T> getAnnotatedType(Class<T> type, String id);
    public <T> Iterable<AnnotatedType<T>> getAnnotatedTypes(Class<T> type);
}

Add Beans Programmatically

class TransformationExtension implements Extension {

  Set<String> transformations = new HashSet<>();

  void collectConfiguration(@Observes ProcessInjectionPoint<?, Transformer> pit) {
    Annotated annotated = pit.getInjectionPoint().getAnnotated();
    if (annotated.isAnnotationPresent(Transformation.class))
      transformations.add(annotated.getAnnotation(Transformation.class).value());
  }
  void addLegacyTransformerBean(@Observes AfterBeanDiscovery abd) {
    LegacyTransformer legacyTransformer = new LegacyTransformer(transformations);
    abd.addBean(new LegacyTransformerBean>(transformations));
  }
}

Generics as Metadata

Parameterized types are not erased by CDI
@Produces
@Transformation
<I, O> Transformer<I, O> legacyTransformerFacade(LegacyTransformer legacyTransformer,
  InjectionPoint injectionPoint) {
  Transformation transformation = getQualifierByType(injectionPoint.getQualifiers(),
                                                     Transformation.class);

  return new LegacyTransformerFacade<I, O>(legacyTransformer, transformation.value(),
    injectionPoint.getType()));
}

Camel CDI

Annotated types, events, injection Targets, transactional Observers

Apache Camel

Open-source integration framework based on known Enterprise Integration Patterns
Bean binding and integration with Spring, Blueprint, Guice and CDI
eip

Camel Annotations

@EndpointInject(uri="jms:queue:foo")
Endpoint endpoint;

@PropertyInject(value = "timeout", defaultValue = "5000")
int timeout;

@BeanInject("foo")
FooBean foo;

@Produce(uri = "mock:foo")
ProducerTemplate producer;

@Consume(uri="jms:queue:foo")
void onFoo(@Body String body) {
}
Bring support for both Camel and CDI beans…​

The ProcessAnnotatedType Event

AnnotatedType<X>
public interface AnnotatedType<X> extends Annotated {
    public Class<X> getJavaClass();
    public Set<AnnotatedConstructor<X>> getConstructors();
    public Set<AnnotatedMethod<? super X>> getMethods();
    public Set<AnnotatedField<? super X>> getFields();
}
ProcessAnnotatedType<X>
public interface ProcessAnnotatedType<X> {
    public AnnotatedType<X> getAnnotatedType();
    public void setAnnotatedType(AnnotatedType<X> type);
    public void veto();
}

The ProcessInjectionTarget Event

InjectionTarget<T>
public interface InjectionTarget<T> extends Producer<T> {
    public void inject(T instance, CreationalContext<T> ctx);
    public void postConstruct(T instance);
    public void preDestroy(T instance);
}
ProcessInjectionTarget<T>
public interface ProcessInjectionTarget<X> {
    public AnnotatedType<X> getAnnotatedType();
    public InjectionTarget<X> getInjectionTarget();
    public void setInjectionTarget(InjectionTarget<X> injectionTarget);
    public void addDefinitionError(Throwable t);
}

Bean Post Processors

class CdiCamelExtension implements Extension {
  Set<AnnotatedType<?>> camelBeans = new HashSet<>());

  void camelAnnotations(@Observes @WithAnnotations({BeanInject.class, (1)
      Consume.class, EndpointInject.class, Produce.class, PropertyInject.class})
      ProcessAnnotatedType<?> pat) {
        camelBeans.add(pat.getAnnotatedType());
  }

  <T> void camelBeansPostProcessor(@Observes ProcessInjectionTarget<T> pit) {
      if (camelBeans.contains(pit.getAnnotatedType())) (2)
        pit.setInjectionTarget(new CamelInjectionTarget<>(pit.getInjectionTarget()));
  }
}
1Detect all the types containing Camel annotations with @WithAnnotations
2Decorate the InjectionTarget corresponding to these types with a custom post-processor

InjectionTarget Decoration

class CamelInjectionTarget<T> implements InjectionTarget<T> {
    InjectionTarget<T> delegate;
    DefaultCamelBeanPostProcessor processor;

    CamelInjectionTarget(InjectionTarget<T> target) {
        delegate = target;
        processor = new DefaultCamelBeanPostProcessor();
    }

    @Override
    public void inject(T instance, CreationalContext<T> ctx) {
        delegate.inject(instance, ctx);
        processor.postProcessBeforeInitialization(instance); (1)
    }
}
1Call the Camel default bean post-processor after CDI injection

Camel DSL

from("jms:queue:{{input}}?transactionManager=#jtaTM")
  .id("Input Consumer")
  .onException().log("Rolling back message with ID ${header.JMSMessageID}")
    .rollback().id("Rollback Transaction")
    .end()
  .log("Receiving message with ID ${header.JMSMessageID}: ${body}")
  .choice()
    .when(header("JMSRedelivered").isEqualTo(Boolean.TRUE))
      .to("jms:queue:{{error}}?transactionManager=#jtaTM").id("Error Producer")
    .otherwise()
      .beanRef("transformer").id("Transformer")
      .to("murex:trade-repository").id("Trade Repository")
      .choice()
        .when(not(isInserted))
          .log("Error received: ${body}").id("Trade Repository Error")
          .throwException(new CamelExecutionException("Import Failed")))
        .otherwise()
          .log("Answer received: ${body}").id("Trade Repository Answer");

Camel AOP

Camel DSL Aspect Oriented Programming with CDI observer methods as pointcut and advice definitions
void interceptProcessor(@Observes @Before @Node("foo") Exchange exchange) {
    // intercept the exchange before processor with id "foo"
}
void interceptProcessorBody(@Observes @Node("foo") @Body String body) {
    // use Camel parameter binding annotations for the joint point context
}
void receive(@Observes(during=AFTER_SUCCESS) @Endpoint("bar") Exchange exchange) {
    // exchange sent to endpoint "bar" when the transaction is committed successfully
}

The ProcessObserverMethod Event

ObserverMethod<T>
public interface ObserverMethod<T> {
    public Class<?> getBeanClass();
    public Type getObservedType();
    public Set<Annotation> getObservedQualifiers();
    public Reception getReception();
    public TransactionPhase getTransactionPhase();
    public void notify(T event);
}
ProcessObserverMethod<T, X>
public interface ProcessObserverMethod<T, X> {
    public AnnotatedMethod<X> getAnnotatedMethod();
    public ObserverMethod<T> getObserverMethod();
    public void addDefinitionError(Throwable t);
}

Camel CDI Extension

Metrics CDI

Annotated types, alternatives, interceptors, producers

Dropwizard Metrics

Open-source Java library providing monitoring primitives like Counter, Gauge, Histogram, Meter, Timer, …​
Provides a MetricRegistry that articulates modules and reporters
Defines annotations for AOP frameworks like Spring AOP, AspectJ, Guice (AOP Alliance) and CDI, e.g.:
class TimedMethodBean {

    @Timed
    void timedMethod() {
        // Timer name => TimedMethodBean.timedMethod
    }
}

Add Interceptor Bindings Programmatically

Use interceptors for Metrics annotation AOP
class MetricsExtension implements Extension {

  <X> void interceptTimedMethod(@Observes @WithAnnotations(Timed.class)
    ProcessAnnotatedType<X> pat) {
    Set<AnnotatedMethod<? super X>> decoratedMethods = new HashSet<>();

    for (AnnotatedMethod<? super X> method : pat.getAnnotatedType().getMethods())
      if (method.isAnnotationPresent(Timed.class))
          decoratedMethods.add(
            new AnnotatedMethodDecorator<>(method, new TimedBindingLiteral()));

    pat.setAnnotatedType(
      new AnnotatedTypeDecorator<>(pat.getAnnotatedType(), decoratedMethods));
  }
}

Bean Method Interceptors

@Interceptor
@TimedBinding
@Priority(Interceptor.Priority.LIBRARY_BEFORE)
class TimedInterceptor {

    @Inject MetricRegistry registry;

    @AroundInvoke
    Object timedMethod(InvocationContext context) throws Exception {
        String name = context.getMethod().getAnnotation(Timed.class).name();
        Timer timer = registry.timer(name);
        Timer.Context time = timer.time();
        try {
            return context.proceed();
        } finally {
            time.stop();
        }
    }
}

The ProcessBean<X> Events

Use producer fields / methods to register custom metrics
@Produces
Timer Timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));
ProcessProducerMethod<T, X>
public interface ProcessProducerMethod<T, X> extends ProcessBean<X> {
    public AnnotatedMethod<T> getAnnotatedProducerMethod();
    public AnnotatedParameter<T> getAnnotatedDisposedParameter();
    // ProcessBean<X>
    public Annotated getAnnotated();
    public Bean<X> getBean();
}
ProcessManagedBean<X> and ProcessProducerField<T, X> are fired for managed beans and producer fields respectively

The AfterDeploymentValidation Event

class MetricsExtension implements Extension {
  Map<Bean<?>, AnnotatedMember<?>> metrics = new HashMap<>();

  void producerFields(@Observes ProcessProducerField<? extends Metric, ?> ppf) {
    metrics.put(ppf.getBean(), ppf.getAnnotatedProducerField()); (1)
  }
  void producerMethods(@Observes ProcessProducerMethod<? extends Metric, ?> ppm) {
    metrics.put(ppm.getBean(), ppm.getAnnotatedProducerMethod()); (1)
  }
  void customMetrics(@Observes AfterDeploymentValidation adv, BeanManager manager) {
    for (Map.Entry<Bean<?>, AnnotatedMember<?>> metric : metrics.entrySet())
      registry.register(metricName(member), manager.getReference(metric.getKey(), (2)
        metric.getValue().getBaseType(), manager.createCreationalContext(null)));
  }
}
1Collect the custom Metric producer fields and methods
2Instantiate the custom metrics into the Metrics registry

Complete Example

@Inject
private Meter hits; (1)

@Timed(name = "calls") (2)
public void cachedMethod() {
    if (hit) hits.mark();
}

@Produces @Metric(name = "cache-hits") (3)
private Gauge<Double> cacheHitRatioGauge(Meter hits, Timer calls) {
    return () -> calls.getOneMinuteRate() == 0 ? Double.NaN :
                 hits.getOneMinuteRate() / calls.getOneMinuteRate();
}
1Metric injection from the registry
2Method instrumentation with CDI interceptors
3Produce a custom Metric instance by composing others

References

Slides generated with Asciidoctor, PlantUML and DZSlides backend
Original slide template - Dan Allen & Sarah White
Camel CDI Extension - github.com/astefanutti/camel-cdi
Metrics CDI Extension - github.com/astefanutti/metrics-cdi

Antoine Sabot-Durand Antonin Stefanutti